//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//  ReminderOutlookUtils.cs
//    
// Abstract:
//
//  Utility class for Outlook extension
//  
//--

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using Microsoft.EnhancedReminders.Properties;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// Utility class for Outlook extension
    /// </summary>
    internal static class ReminderOutlookUtils
    {
        static internal char[] Integers = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

        /// <summary>
        /// Drop down items for Time combo
        /// </summary>
        static internal string[] DefaultReminderTimes = new string[] { Resources.None_Option, 
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, 0),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, 5),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, 10),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, 15),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, 30),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHour, 1),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 2),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 3),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 4),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 5),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 6),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 7),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 8),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 9),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 10),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 11),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, 0.5),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, 18),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatDay, 1),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, 2),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, 3),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, 4),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatWeek, 1),
                                                                    string.Format(CultureInfo.CurrentUICulture, Resources.FormatWeeks, 2)};


        // No. of minutes in an hour
        private const int MinPerHour = 60;

        // No. of minutes in a day
        private const int MinPerDay = 24 * MinPerHour;

        // No. of minutes in a week
        private const int MinPerWeek = 7 * MinPerDay;

        /// <summary>
        /// Extract all integers at the beginning of the string
        /// </summary>
        internal static int ExtractTimeComponent(string text)
        {
            Debug.Assert(text != null, "Time component shouldn't be null");

            string time = String.Empty;
            int result;

            foreach (char c in text.ToCharArray())
            {
                if (Char.IsDigit(c))
                {
                    time += c;
                }
                else
                {
                    break;
                }
            }

            if (!Int32.TryParse(time, out result))
            {
                result = 0;
            };

            return result;
        }

        /// <summary>
        /// Invoked by TimeCombo_OnChanged to parse user input. Follow Outlook's Reminder time combo
        /// behavior where the ending text is parsed to determine whether it is either minute(s), hour(s)
        /// or day(s). If input text is not recognized, the previous input is used.
        /// Examples:
        /// 5 minutes 6 hours -> 5 hours (starting digit + ending text)
        /// 5 0 minutes -> 5 minutes (space treated as digit terminator)
        /// 5 years -> Invalid (previous input used)
        /// 1 minutes -> 1 minute (plurarity corrected)
        /// </summary>
        /// <returns>Time for notification in minutes before the due time</returns>
        internal static bool TryParseRemindTime(string text, out int minutes)
        {
            Debug.Assert(text != null, "Time string shouldn't be null");
            Debug.Assert(text.Trim() == text, "text was trimmed by caller");

            minutes = NotificationChannelListLeadTime.None;

            if (text.Equals(Resources.None_Option, StringComparison.CurrentCultureIgnoreCase))
            {
                return true;
            }

            if (text.IndexOfAny(Integers, 0) != 0)
            {
                return false;
            }

            if (text.EndsWith(Resources.Minute, StringComparison.CurrentCultureIgnoreCase))
            {
                minutes = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Minute, StringComparison.CurrentCultureIgnoreCase)));
                return true;
            }
            else if (text.EndsWith(Resources.Minutes, StringComparison.CurrentCultureIgnoreCase))
            {
                minutes = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Minutes, StringComparison.CurrentCultureIgnoreCase)));
                return true;
            }
            else if (text.EndsWith(Resources.Hour, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Hour, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerHour;
                return true;
            }
            else if (text.EndsWith(Resources.Hours, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Hours, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerHour;
                return true;
            }
            else if (text.EndsWith(Resources.Day, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Day, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerDay;
                return true;
            }
            else if (text.EndsWith(Resources.Days, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Days, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerDay;
                return true;
            }
            else if (text.EndsWith(Resources.Week, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Week, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerWeek;
                return true;
            }
            else if (text.EndsWith(Resources.Weeks, StringComparison.CurrentCultureIgnoreCase))
            {
                int time = ExtractTimeComponent(text.Substring(0, text.IndexOf(Resources.Weeks, StringComparison.CurrentCultureIgnoreCase)));
                minutes = time * MinPerWeek;
                return true;
            }

            // For all other input, we return false to indicate an invalid user input
            return false;
        }

        /// <summary>
        /// Returns formatted time string. 
        /// If minutes == -1, returns None
        /// If minutes is exactly divisible by 60 (hours), we return either
        /// 1. n week(s)
        /// 2. n day(s)
        /// 3. n hour(s)
        /// 4. 0.5 day 
        /// For all other cases, we return 'x minutes'
        /// </summary>
        internal static string FormatRemindTime(int minutes)
        {
            if (minutes == NotificationChannelListLeadTime.None)
            {
                return Resources.None_Option;
            }
            else if (minutes >= MinPerHour && minutes % MinPerHour == 0)
            {
                if (minutes % MinPerWeek == 0)
                {
                    if (minutes / MinPerWeek == 1)
                    {
                        return string.Format(CultureInfo.CurrentUICulture, Resources.FormatWeek, minutes / MinPerWeek);
                    }
                    else
                    {
                        return string.Format(CultureInfo.CurrentUICulture, Resources.FormatWeeks, minutes / MinPerWeek);
                    }
                }
                else if (minutes % MinPerDay == 0)
                {
                    if (minutes / MinPerDay == 1)
                    {
                        return string.Format(CultureInfo.CurrentUICulture, Resources.FormatDay, minutes / MinPerDay);
                    }
                    else
                    {
                        return string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, minutes / MinPerDay);
                    }
                }
                else
                {
                    Debug.Assert(minutes % MinPerHour == 0, "Minutes should be divisible by hour");

                    if (minutes / MinPerHour == 1)
                    {
                        return string.Format(CultureInfo.CurrentUICulture, Resources.FormatHour, minutes / MinPerHour);
                    }
                    else
                    {
                        if (minutes / MinPerHour == 12)
                        {
                            return string.Format(CultureInfo.CurrentUICulture, Resources.FormatDays, 0.5);
                        }
                        else
                        {
                            return string.Format(CultureInfo.CurrentUICulture, Resources.FormatHours, minutes / MinPerHour);
                        }
                    }
                }
            }
            else if (minutes == 1)
            {
                return string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinute, minutes);
            }
            else
            {
                return string.Format(CultureInfo.CurrentUICulture, Resources.FormatMinutes, minutes);
            }
        }

        /// <summary>
        /// Load the storage data object from the Outlook storage item.
        /// </summary>
        /// <returns>
        /// A NotificationStorageData instance or null if the storage item could not be loaded.
        /// </returns>
        private static NotificationStorageData LoadStorageData(IOutlookApplication application)
        {
            Debug.Assert(application != null, "application != null");

            IStorageItem rulesStorageItem = application.GetStorageItem();

            if (rulesStorageItem != null)
            {
                // This might return null if the storage item does not have the correct attachment
                return OutlookUtils.LoadAttachment(rulesStorageItem.Attachments) as NotificationStorageData;
            }
            else
            {
                // Return null to indicate failure
                return null;
            }
        }

        /// <summary>
        /// Save the storage data object to the Outlook storage item.
        /// </summary>
        /// <param name="storageData">The storage data object to save</param>
        /// <param name="application">An instance of IApplication</param>
        internal static void SaveStorageData(NotificationStorageData storageData, IOutlookApplication application)
        {
            Debug.Assert(storageData != null, "storageData != null");
            Debug.Assert(application != null, "application != null");

            IStorageItem storageItem = application.GetStorageItem();

            Debug.Assert(storageItem != null, "storageItem != null");

            OutlookUtils.RemoveAttachment(storageItem.Attachments);
            OutlookUtils.SaveAttachment(storageItem.Attachments, storageData);

            storageItem.Save();
        }

        /// <summary>
        /// Load Enhanced Reminder rules and copy the content into the NotificationRules instance
        /// that is passed in.
        /// </summary>
        /// <exception cref="InvalidOperationException">Throws InvalidOperationException when rules are not available</exception>
        internal static void LoadRules(IOutlookApplication application, ref NotificationRules rules)
        {
            Debug.Assert(application != null, "application != null");
            Debug.Assert(rules.Count == 0, "rules.Count == 0");

            NotificationRules loadedRules = LoadRules(application);

            if (loadedRules == null)
            {
                throw new InvalidOperationException(Resources.RulesNotAvailable);
            }

            foreach (NotificationRuleItem rule in loadedRules)
            {
                rules.Add(rule);
            }

            // Update all high level properties
            rules.DoNotPromptUserOnDelete = loadedRules.DoNotPromptUserOnDelete;
            rules.TimeZone = loadedRules.TimeZone;
        }

        /// <summary>
        /// Load Enhanced Reminders rules. Deserializing errors will throw exceptions that the
        /// caller is responsible for handling.
        /// </summary>
        /// <param name="application">An instance of IApplication</param>
        /// <returns>NotificationRules object</returns>
        private static NotificationRules LoadRules(IOutlookApplication application) 
        {
            Debug.Assert(application != null, "application != null");

            NotificationStorageData storageData = LoadStorageData(application);

            return storageData != null ? storageData.Rules : null;
        }

        /// <summary>
        /// Save Enhanced Reminders rules. Serialization errors will throw exceptions that the
        /// caller is responsible for handling
        /// </summary>
        /// <param name="rules">NotificationRules object</param>
        /// <param name="application">An instance of IApplication</param>
        internal static void SaveRules(NotificationRules rules, IOutlookApplication application)
        {
            Debug.Assert(rules != null, "rules != null");
            Debug.Assert(application != null, "application != null");

            // Re-load the storage data to ensure rules are stored with current channels.
            NotificationStorageData storageData = LoadStorageData(application);

            if (storageData == null)
            {
                Logger.WriteTraceError("Unable to re-load Outlook storage data prior to saving rules");
                return;
            }

            // Ensure the calendar item rule IDs are updated correctly

            rules.UpdateRuleIds(storageData.Rules);

            storageData.Rules = rules;

            SaveStorageData(storageData, application);
        }

        /// <summary>
        /// Load Enhanced Reminders channels.  Optionally restore the default channels if channels could not be loaded (e.g. due to a deserialization error).
        /// In this case a warning dialog is displayed and the default channels are then re-created and returned.
        /// </summary>
        /// <param name="application">An instance of IApplication</param>
        /// <param name="restoreDefaultsIfNecessary">True to re-create default channels and rules if channel loading fails; false otherwise.</param>
        /// <returns>The loaded channels or null if loading failed and we didn't want to restore the defaults.</returns>
        internal static List<NotificationChannel> LoadChannels(IOutlookApplication application, bool restoreDefaultsIfNecessary)
        {
            Debug.Assert(application != null, "application != null");

            NotificationStorageData storageData = LoadStorageData(application);
            List<NotificationChannel> channels = null;

            if (storageData != null && storageData.MasterChannelList != null)
            {
                channels = storageData.MasterChannelList.Channels;
            }

            if (channels != null || !restoreDefaultsIfNecessary)
            {
                return channels; // Channels loaded successfully or they failed to load and we don't want to restore defaults.
            }

            // Error loading channels.  Most likely the version changed.  Warn the user that loading 
            // channels failed and default channels/rules will be restored.
            MessageBox.Show(Properties.Resources.ChannelsLoadWarning,
                            Properties.Resources.ChannelControlWarningTitle,
                            MessageBoxButton.OK,
                            MessageBoxImage.Warning);

            storageData = OutlookUtils.CreateDefaultStorageData();
            ReminderOutlookUtils.SaveStorageData(storageData, application);

            Debug.Assert(storageData.MasterChannelList != null, "storageData.Channels");
            Debug.Assert(storageData.MasterChannelList.Channels != null, "storageData.Channels.Channels");

            return storageData.MasterChannelList.Channels;
        }

        /// <summary>
        /// Save the default channels to use for Enhanced Reminders. Serialization errors will throw exceptions that the
        /// caller is responsible for handling
        /// </summary>
        /// <param name="channels">The default channel list</param>
        /// <param name="application">An instance of IApplication</param>
        internal static void SaveChannels(IEnumerable<NotificationChannel> channels, IOutlookApplication application)
        {
            NotificationStorageData storageData = LoadStorageData(application);

            if (storageData == null)
            {
                Logger.WriteTraceError("Unable to re-load Outlook storage data prior to saving channels");
                return;
            }

            storageData.MasterChannelList = new NotificationChannelList(channels);

            SaveStorageData(storageData, application);
        }
    }
}
